 <!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>流式AI电话通信测试</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            color: #333;
        }

        .container {
            background: white;
            border-radius: 20px;
            padding: 2rem;
            box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
            max-width: 600px;
            width: 90%;
        }

        .header {
            text-align: center;
            margin-bottom: 2rem;
        }

        .header h1 {
            color: #667eea;
            margin-bottom: 0.5rem;
        }

        .header p {
            color: #666;
            font-size: 0.9rem;
        }

        .status-panel {
            background: #f8f9fa;
            border-radius: 10px;
            padding: 1rem;
            margin-bottom: 1rem;
            border-left: 4px solid #667eea;
        }

        .controls {
            display: flex;
            gap: 1rem;
            justify-content: center;
            margin: 2rem 0;
            flex-wrap: wrap;
        }

        .btn {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            border-radius: 25px;
            padding: 12px 24px;
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s ease;
            display: flex;
            align-items: center;
            gap: 8px;
            min-width: 150px;
            justify-content: center;
        }

        .btn:hover:not(:disabled) {
            transform: translateY(-2px);
            box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
        }

        .btn:disabled {
            background: #ccc;
            cursor: not-allowed;
            transform: none;
            box-shadow: none;
        }

        .btn.streaming {
            background: linear-gradient(135deg, #00d4ff 0%, #0099cc 100%);
        }

        .btn.streaming:hover:not(:disabled) {
            box-shadow: 0 6px 20px rgba(0, 212, 255, 0.4);
        }

        .btn.active {
            background: linear-gradient(135deg, #ff4757 0%, #ff3742 100%);
            animation: pulse 2s infinite;
        }

        @keyframes pulse {
            0% {
                box-shadow: 0 0 0 0 rgba(255, 71, 87, 0.7);
            }
            70% {
                box-shadow: 0 0 0 15px rgba(255, 71, 87, 0);
            }
            100% {
                box-shadow: 0 0 0 0 rgba(255, 71, 87, 0);
            }
        }

        .messages {
            background: #f8f9fa;
            border-radius: 10px;
            padding: 1rem;
            max-height: 300px;
            overflow-y: auto;
            margin-bottom: 1rem;
        }

        .message {
            margin-bottom: 0.5rem;
            padding: 0.5rem;
            border-radius: 8px;
            font-size: 0.9rem;
        }

        .message.system {
            background: #e3f2fd;
            color: #1976d2;
        }

        .message.user {
            background: #e8f5e8;
            color: #2e7d32;
        }

        .message.ai {
            background: #fff3e0;
            color: #f57c00;
        }

        .message.streaming {
            background: #e0f7ff;
            color: #0288d1;
            border-left: 4px solid #00d4ff;
        }

        .stats {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
            gap: 1rem;
            margin-top: 1rem;
        }

        .stat-item {
            background: white;
            border-radius: 8px;
            padding: 1rem;
            text-align: center;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
        }

        .stat-value {
            font-size: 1.5rem;
            font-weight: bold;
            color: #667eea;
        }

        .stat-label {
            font-size: 0.8rem;
            color: #666;
            margin-top: 0.25rem;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>🌊 流式AI电话通信测试</h1>
            <p>测试新一代流式语音交互技术</p>
        </div>

        <div class="status-panel">
            <div id="connectionStatus">
                <strong>连接状态:</strong> <span id="statusText">未连接</span>
            </div>
            <div id="callStatus" style="display: none;">
                <strong>通话状态:</strong> <span id="callStatusText">待机</span>
            </div>
        </div>

        <div class="controls">
            <button id="connectBtn" class="btn">
                📡 连接WebSocket
            </button>
            <button id="streamingCallBtn" class="btn streaming" disabled>
                🌊 流式通话
            </button>
            <button id="normalCallBtn" class="btn" disabled>
                📞 普通通话
            </button>
        </div>

        <div class="messages" id="messages">
            <div class="message system">
                💡 欢迎使用流式AI电话通信测试！请先连接WebSocket。
            </div>
        </div>

        <div class="stats">
            <div class="stat-item">
                <div class="stat-value" id="sentChunks">0</div>
                <div class="stat-label">发送音频块</div>
            </div>
            <div class="stat-item">
                <div class="stat-value" id="receivedResponses">0</div>
                <div class="stat-label">收到回复</div>
            </div>
            <div class="stat-item">
                <div class="stat-value" id="avgResponseTime">0</div>
                <div class="stat-label">平均延迟(ms)</div>
            </div>
            <div class="stat-item">
                <div class="stat-value" id="errorCount">0</div>
                <div class="stat-label">错误次数</div>
            </div>
        </div>
    </div>

    <script>
        class StreamingPhoneTest {
            constructor() {
                this.ws = null;
                this.isConnected = false;
                this.isCallActive = false;
                this.mediaRecorder = null;
                this.mediaStream = null;
                this.isStreamingMode = false;
                
                this.stats = {
                    sentChunks: 0,
                    receivedResponses: 0,
                    totalResponseTime: 0,
                    errors: 0
                };
                
                this.initElements();
                this.bindEvents();
            }
            
            initElements() {
                this.connectBtn = document.getElementById('connectBtn');
                this.streamingCallBtn = document.getElementById('streamingCallBtn');
                this.normalCallBtn = document.getElementById('normalCallBtn');
                this.statusText = document.getElementById('statusText');
                this.callStatusText = document.getElementById('callStatusText');
                this.callStatus = document.getElementById('callStatus');
                this.messages = document.getElementById('messages');
            }
            
            bindEvents() {
                this.connectBtn.addEventListener('click', () => this.toggleConnection());
                this.streamingCallBtn.addEventListener('click', () => this.toggleStreamingCall());
                this.normalCallBtn.addEventListener('click', () => this.toggleNormalCall());
            }
            
            toggleConnection() {
                if (this.isConnected) {
                    this.disconnect();
                } else {
                    this.connect();
                }
            }
            
            connect() {
                try {
                    this.ws = new WebSocket('ws://localhost:8123/api/ws/voice-chat');
                    
                    this.ws.onopen = () => {
                        this.isConnected = true;
                        this.statusText.textContent = '已连接';
                        this.statusText.style.color = '#4caf50';
                        this.connectBtn.textContent = '📡 断开连接';
                        this.streamingCallBtn.disabled = false;
                        this.normalCallBtn.disabled = false;
                        this.addMessage('system', '✅ WebSocket连接成功！');
                    };
                    
                    this.ws.onmessage = (event) => {
                        this.handleMessage(JSON.parse(event.data));
                    };
                    
                    this.ws.onclose = () => {
                        this.isConnected = false;
                        this.statusText.textContent = '连接断开';
                        this.statusText.style.color = '#f44336';
                        this.connectBtn.textContent = '📡 连接WebSocket';
                        this.streamingCallBtn.disabled = true;
                        this.normalCallBtn.disabled = true;
                        this.addMessage('system', '❌ WebSocket连接断开');
                    };
                    
                    this.ws.onerror = (error) => {
                        this.addMessage('system', '❌ WebSocket错误: ' + error.message);
                        this.updateStats('errors', this.stats.errors + 1);
                    };
                    
                } catch (error) {
                    this.addMessage('system', '❌ 连接失败: ' + error.message);
                }
            }
            
            disconnect() {
                if (this.ws) {
                    this.ws.close();
                }
                if (this.isCallActive) {
                    this.endCall();
                }
            }
            
            toggleStreamingCall() {
                if (this.isCallActive) {
                    this.endCall();
                } else {
                    this.startStreamingCall();
                }
            }
            
            toggleNormalCall() {
                if (this.isCallActive) {
                    this.endCall();
                } else {
                    this.startNormalCall();
                }
            }
            
            async startStreamingCall() {
                try {
                    this.isStreamingMode = true;
                    this.addMessage('system', '🌊 启动流式通话...');
                    
                    // 获取麦克风权限
                    this.mediaStream = await navigator.mediaDevices.getUserMedia({
                        audio: {
                            echoCancellation: true,
                            noiseSuppression: true,
                            autoGainControl: true,
                            sampleRate: 24000
                        }
                    });
                    
                    // 创建MediaRecorder
                    this.mediaRecorder = new MediaRecorder(this.mediaStream, {
                        mimeType: 'audio/webm;codecs=opus'
                    });
                    
                    let audioChunks = [];
                    let isFirstChunk = true;
                    
                    this.mediaRecorder.ondataavailable = async (event) => {
                        if (event.data.size > 0 && event.data.size < 50000) {
                            audioChunks.push(event.data);
                            
                            if (audioChunks.length >= 1) {
                                const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
                                if (audioBlob.size < 100000) {
                                    await this.sendStreamingAudio(audioBlob, isFirstChunk);
                                    isFirstChunk = false;
                                }
                                audioChunks = [];
                            }
                        }
                    };
                    
                    // 开始录音 - 500ms间隔
                    this.mediaRecorder.start(500);
                    
                    this.isCallActive = true;
                    this.streamingCallBtn.textContent = '📴 结束流式通话';
                    this.streamingCallBtn.classList.add('active');
                    this.normalCallBtn.disabled = true;
                    this.callStatus.style.display = 'block';
                    this.callStatusText.textContent = '流式通话中';
                    this.callStatusText.style.color = '#00d4ff';
                    
                    this.addMessage('streaming', '🌊 流式通话已开始，AI将流式回复...');
                    
                } catch (error) {
                    this.addMessage('system', '❌ 启动流式通话失败: ' + error.message);
                    this.updateStats('errors', this.stats.errors + 1);
                }
            }
            
            async startNormalCall() {
                try {
                    this.isStreamingMode = false;
                    this.addMessage('system', '📞 启动普通通话...');
                    
                    // 获取麦克风权限
                    this.mediaStream = await navigator.mediaDevices.getUserMedia({
                        audio: {
                            echoCancellation: true,
                            noiseSuppression: true,
                            autoGainControl: true
                        }
                    });
                    
                    // 创建MediaRecorder
                    this.mediaRecorder = new MediaRecorder(this.mediaStream, {
                        mimeType: 'audio/webm;codecs=opus'
                    });
                    
                    let audioChunks = [];
                    let isFirstChunk = true;
                    
                    this.mediaRecorder.ondataavailable = async (event) => {
                        if (event.data.size > 0) {
                            audioChunks.push(event.data);
                            
                            if (audioChunks.length >= 2) {
                                const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
                                await this.sendNormalAudio(audioBlob, isFirstChunk);
                                isFirstChunk = false;
                                audioChunks = [];
                            }
                        }
                    };
                    
                    // 开始录音 - 1000ms间隔
                    this.mediaRecorder.start(1000);
                    
                    this.isCallActive = true;
                    this.normalCallBtn.textContent = '📴 结束普通通话';
                    this.normalCallBtn.classList.add('active');
                    this.streamingCallBtn.disabled = true;
                    this.callStatus.style.display = 'block';
                    this.callStatusText.textContent = '普通通话中';
                    this.callStatusText.style.color = '#667eea';
                    
                    this.addMessage('system', '📞 普通通话已开始');
                    
                } catch (error) {
                    this.addMessage('system', '❌ 启动普通通话失败: ' + error.message);
                    this.updateStats('errors', this.stats.errors + 1);
                }
            }
            
            endCall() {
                if (this.mediaRecorder && this.mediaRecorder.state === 'recording') {
                    this.mediaRecorder.stop();
                }
                
                if (this.mediaStream) {
                    this.mediaStream.getTracks().forEach(track => track.stop());
                }
                
                this.isCallActive = false;
                this.streamingCallBtn.textContent = '🌊 流式通话';
                this.normalCallBtn.textContent = '📞 普通通话';
                this.streamingCallBtn.classList.remove('active');
                this.normalCallBtn.classList.remove('active');
                this.streamingCallBtn.disabled = false;
                this.normalCallBtn.disabled = false;
                this.callStatus.style.display = 'none';
                
                this.addMessage('system', '📴 通话已结束');
            }
            
            async sendStreamingAudio(audioBlob, isFirstChunk) {
                const reader = new FileReader();
                reader.onload = () => {
                    const base64Audio = reader.result.split(',')[1];
                    const request = {
                        audioBase64: base64Audio,
                        sessionId: 'test-session-' + Date.now(),
                        isRealtime: true,
                        isFirstChunk: isFirstChunk,
                        sendTime: Date.now(),
                        textInput: '流式' // 标识使用流式处理
                    };
                    
                    this.ws.send(JSON.stringify(request));
                    this.updateStats('sentChunks', this.stats.sentChunks + 1);
                };
                reader.readAsDataURL(audioBlob);
            }
            
            async sendNormalAudio(audioBlob, isFirstChunk) {
                const reader = new FileReader();
                reader.onload = () => {
                    const base64Audio = reader.result.split(',')[1];
                    const request = {
                        audioBase64: base64Audio,
                        sessionId: 'test-session-' + Date.now(),
                        isRealtime: true,
                        isFirstChunk: isFirstChunk,
                        sendTime: Date.now()
                    };
                    
                    this.ws.send(JSON.stringify(request));
                    this.updateStats('sentChunks', this.stats.sentChunks + 1);
                };
                reader.readAsDataURL(audioBlob);
            }
            
            handleMessage(data) {
                console.log('收到消息:', data);
                
                if (data.type === 'voice-response') {
                    this.handleVoiceResponse(data.data);
                } else if (data.type && data.type.startsWith('streaming_')) {
                    this.handleStreamingMessage(data);
                }
            }
            
            handleVoiceResponse(response) {
                this.updateStats('receivedResponses', this.stats.receivedResponses + 1);
                
                if (response.processingTime) {
                    this.stats.totalResponseTime += response.processingTime;
                    const avgTime = Math.round(this.stats.totalResponseTime / this.stats.receivedResponses);
                    this.updateStats('avgResponseTime', avgTime);
                }
                
                if (response.status === 'success') {
                    if (response.recognizedText) {
                        this.addMessage('user', '🎤 识别: ' + response.recognizedText);
                    }
                    if (response.aiResponse) {
                        this.addMessage('ai', '🤖 回复: ' + response.aiResponse);
                    }
                    if (response.audioBase64 && response.audioBase64.length > 0) {
                        this.playAudio(response.audioBase64);
                    }
                } else {
                    this.addMessage('system', '❌ 错误: ' + response.errorMessage);
                    this.updateStats('errors', this.stats.errors + 1);
                }
            }
            
            handleStreamingMessage(data) {
                const type = data.type.replace('streaming_', '');
                
                switch (type) {
                    case 'connected':
                        this.addMessage('streaming', '🌊 ' + data.content);
                        break;
                    case 'recognition':
                        this.addMessage('user', '🎤 识别: ' + data.content);
                        break;
                    case 'ai_thinking':
                        this.addMessage('streaming', '🤔 ' + data.content);
                        break;
                    case 'ai_response':
                        this.addMessage('ai', '🌊 AI: ' + data.content);
                        this.updateStats('receivedResponses', this.stats.receivedResponses + 1);
                        break;
                    case 'audio':
                        if (data.audioBase64 && data.audioBase64.length > 0) {
                            this.playAudio(data.audioBase64);
                        }
                        if (data.processingTime) {
                            this.stats.totalResponseTime += data.processingTime;
                            const avgTime = Math.round(this.stats.totalResponseTime / this.stats.receivedResponses);
                            this.updateStats('avgResponseTime', avgTime);
                        }
                        break;
                }
            }
            
            playAudio(audioBase64) {
                try {
                    const audioBlob = this.base64ToBlob(audioBase64, 'audio/wav');
                    const audioUrl = URL.createObjectURL(audioBlob);
                    const audio = new Audio(audioUrl);
                    audio.play().catch(error => {
                        console.error('音频播放失败:', error);
                    });
                } catch (error) {
                    console.error('音频处理失败:', error);
                }
            }
            
            base64ToBlob(base64, mimeType) {
                const byteCharacters = atob(base64);
                const byteNumbers = new Array(byteCharacters.length);
                for (let i = 0; i < byteCharacters.length; i++) {
                    byteNumbers[i] = byteCharacters.charCodeAt(i);
                }
                const byteArray = new Uint8Array(byteNumbers);
                return new Blob([byteArray], { type: mimeType });
            }
            
            addMessage(type, content) {
                const messageDiv = document.createElement('div');
                messageDiv.className = `message ${type}`;
                messageDiv.textContent = `${new Date().toLocaleTimeString()} ${content}`;
                this.messages.appendChild(messageDiv);
                this.messages.scrollTop = this.messages.scrollHeight;
            }
            
            updateStats(key, value) {
                this.stats[key] = value;
                const element = document.getElementById(key === 'avgResponseTime' ? 'avgResponseTime' : 
                                                      key === 'sentChunks' ? 'sentChunks' :
                                                      key === 'receivedResponses' ? 'receivedResponses' : 'errorCount');
                if (element) {
                    element.textContent = value;
                }
            }
        }
        
        // 启动测试应用
        document.addEventListener('DOMContentLoaded', () => {
            new StreamingPhoneTest();
        });
    </script>
</body>
</html>

